(译) 迭代器和生成器

迭代器和生成器

翻译自:5.Iterators & Generators 5.Iterators & Generators

5.1. 迭代器(Iterators)

首先,我们用for循环来循环遍历一个列表(list)。

>>> for i in [1, 2, 3, 4]:
...     print i,
...
1
2
3
4

假如我们将for循环用在一个字符串中,它将会遍历字符串的每一个字符。

>>> for c in "python":
...     print c
...
p
y
t
h
o
n

假如我们将for循环用在一个字典中,它将遍历字典的每一个key值。

>>> for k in {"x": 1, "y": 2}:
...     print k
...
y
x

假如我们将for循环用在一个文件中,它将遍历该文件的每一行内容。

>>> for line in open("a.txt"):
...     print line,
...
first line
second line

所以说,有很多类型的对象都能被for用来循环遍历。这样的对象也叫做可迭代对象( iterable objects)。

有很多函数可以用来消费这些可迭代者(iterable)。

>>> ",".join(["a", "b", "c"])
'a,b,c'
>>> ",".join({"x": 1, "y": 2})
'y,x'
>>> list("python")
['p', 'y', 't', 'h', 'o', 'n']
>>> list({"x": 1, "y": 2})
['y', 'x']

5.1.1. 迭代协议 (The Iteraton Protocol)

Python内置的函数iter接收一个可迭代对象为参数,返回一个迭代器。

>>> x = iter([1, 2, 3])
>>> x
<listiterator object at 0x1004ca850>
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

每一次当我们访问一个迭代器的next方法,迭代器都为我们生成下一个元素。如果没有更多的元素可生成,它就会抛出一个StopIteration异常。

迭代器以类的方式实现。这里是一个迭代器的实现例子,它的功能类似于Python的内置函数xrange

class yrange:
    def __init__(self, n):
        self.i = 0
        self.n = n

    def __iter__(self):
        return self

    def next(self):
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()

这里,__iter__方法就是能使一个对象变成可迭代者iterable的原因。在幕后,iter函数对给定的对象调用__iter__方法。

__iter__返回的值就是一个迭代器(iterator)。它应该具有一个next方法,并且当没有更多元素的时候会抛出StopIteration异常。

让我们来试验一下:

>>> y = yrange(3)
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 14, in next
StopIteration

许多内置的函数都接受迭代器作为参数。

>>> list(yrange(5))
[0, 1, 2, 3, 4]
>>> sum(yrange(5))
10

上面这个例子中,iterableiterator是同一个对象。注意到__iter__方法返回了它本身,其实并不需要总是这样做。

class zrange:
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        return zrange_iter(self.n)

class zrange_iter:
    def __init__(self, n):
        self.i = 0
        self.n = n

    def __iter__(self):
        # Iterators are iterables too.
        # Adding this functions to make them so.
        return self

    def next(self):
        if self.i < self.n:
            i = self.i
            self.i += 1
            return i
        else:
            raise StopIteration()

如果iteratableiterator是相同的对象,那么它们在一次迭代中就会消耗掉。

>>> y = yrange(5)
>>> list(y)
[0, 1, 2, 3, 4]
>>> list(y)
[]
>>> z = zrange(5)
>>> list(z)
[0, 1, 2, 3, 4]
>>> list(z)
[0, 1, 2, 3, 4]

问题 1: 写一个迭代器类reverse_iter, 接收一个list参数并反向迭代。 ::

>>> it = reverse_iter([1, 2, 3, 4])
>>> it.next()
4
>>> it.next()
3
>>> it.next()
2
>>> it.next()
1
>>> it.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

5.2. 生成器(Generators)

生成器简化了创建迭代器的过程。一个生成器说白了就是一个函数,一个函数能产生一些列的结果而不是只产生一个单一结果的函数。

def yrange(n):
    i = 0
    while i < n:
        yield i
        i += 1

每一次yield语句被执行,该函数就生成一个新的值。

>>> y = yrange(3)
>>> y
<generator object yrange at 0x401f30>
>>> y.next()
0
>>> y.next()
1
>>> y.next()
2
>>> y.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

所以说,生成器本质上是一个迭代器。这里,你不需要操心迭代协议。

"生成器"这个词很含糊,它既用来表示生成东西的函数也表示这个函数所生成的东西。在这章里,我将用"生成器"这个词来表示由函数生成的对象,同时使用另外一个词"生成器函数"来表示生成生成对象的函数。

你能思考一下它内部是怎么工作的吗?

当一个生成器函数被调用的时候,它就返回一个生成器对象,甚至这个函数都还没有开始执行的时候就返回了。当next方法第一次被调用的时候,生成器函数就开始执行,直到它遇到yield语句。由yield生成的值将在下一个next调用的时候被返回。

下面这个例子展示了在一个生成器对象中yield语句和next方法调用之间的相互作用关系。

>>> def foo():
...     print "begin"
...     for i in range(3):
...         print "before yield", i
...         yield i
...         print "after yield", i
...     print "end"
...
>>> f = foo()
>>> f.next()
begin
before yield 0
0
>>> f.next()
after yield 0
before yield 1
1
>>> f.next()
after yield 1
before yield 2
2
>>> f.next()
after yield 2
end
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

让我们看一个例子:

def integers():
    """Infinite sequence of integers."""
    i = 1
    while True:
        yield i
        i = i + 1

def squares():
    for i in integers():
        yield i * i

def take(n, seq):
    """Returns first n values from the given sequence."""
    seq = iter(seq)
    result = []
    try:
        for i in range(n):
            result.append(seq.next())
    except StopIteration:
        pass
    return result
print take(5, squares()) # prints [1, 4, 9, 16, 25]

5.3. 生成器表达式 (Generator Expressions)

生成器表达式是生成器版本的链表推导式。它们看起来像链表推导式,但是不同的是,它返回一个生成器而不是一个链表。

>>> a = (x*x for x in range(10))
>>> a
<generator object <genexpr> at 0x401f08>
>>> sum(a)
285
We can use the generator expressions as arguments to various functions that consume iterators.

>>> sum(((x*x for x in range(10)))
285

当调用程序只有唯一一个参数时,生成器表达式外面的括号可以被省略。

>>> sum(x*x for x in range(10))
285

另外一个有意思的例子:

比如我们想寻找前面10个(或者前面n个)毕氏三元数(pythogorian triplets)。当满足x*x + y*y == z*z时,我们将(x, y, z)叫做毕氏三元数或勾股数。

如果我们知道我们z的最大需要测试的值的话,那么解决这个问题是比较容易的。但是这里我们想找的是前面n个毕氏三元数。

>>> pyt = ((x, y, z) for z in integers() for y in xrange(1, z) for x in range(1, y) if x*x + y*y == z*z)
>>> take(10, pyt)
[(3, 4, 5), (6, 8, 10), (5, 12, 13), (9, 12, 15), (8, 15, 17), (12, 16, 20), (15, 20, 25), (7, 24, 25), (10, 24, 26), (20, 21, 29)]

5.3.1. 例子:读取多个文件 (Example: Reading multiple files)

比如我们想写一个程序,它接收一些文件名作为参数,然后打印出所有这些文件的内容,就像Unix中的cat命令那样。

传统的实现方式是这样子的:

def cat(filenames):
    for f in filenames:
        for line in open(f):
            print line,

现在,假设我们希望只打印那些含有特定子字符串的行,就像 Unix中的grep命令一样。

def grep(pattern, filenames):
    for f in filenames:
        for line in open(f):
            if pattern in line:
                print line,

这两个程序的代码有很多共同点。将这个共同点移到一个函数中有点难。但是有了生成器,事情就变得简单得多了。

def readfiles(filenames):
    for f in filenames:
        for line in open(f):
            yield line

def grep(pattern, lines):
    return (line for line in lines if pattern in lines)

def printlines(lines):
    for line in lines:
        print line,

def main(pattern, filenames):
    lines = readfiles(filenames)
    lines = grep(pattern, lines)
    printlines(lines)

这段代码现在简单多了,它的每一个函数都只负责一件小事情。我们可以将这些所有的小函数都移到一个单独的模块中并在之后的其他程序中重用它们。

问题 2:写一个程序,它接收1个或多个文件名为参数,并打印出所有长度大于40个字符的行。

问题 3:写一个函数findfiles逐层递归一个给定目录的目录树并为这个目录树中的所有文件生成它们的文件路径。

问题 4:写一个函数用来递归地计算一个给定目录下Python文件(.py扩展名)的个数。

问题 5:写一个函数用来递归地计算一个给定目录下所有Python文件的代码行数的总和。

问题 6:写一个函数用来递归地计算一个给定目录下所有Python文件的代码行数的总和,忽略那些空行和注释行。

问题 7:写一个程序split.py,它以命令行参数的形式接收一个整形参数n和一个文件名参数,然后将这个文件分割成若干个小文件,每个小文件有n行。

5.4. Itertools 模块

itertools模块包含在Python标准库中,它提供了很多有趣的工具可以用来配合迭代器工作。

让我们看几个有意思的函数。

chain - 用来将多个迭代器连在一起。

>>> it1 = iter([1, 2, 3])
>>> it2 = iter([4, 5, 6])
>>> itertools.chain(it1, it2)
[1, 2, 3, 4, 5, 6]
izip  iterable version of zip

>>> for x, y in itertools.izip(["a", "b", "c"], [1, 2, 3]):
...     print x, y
...
a 1
b 2
c 3

问题 8: 写一个函数peep,它接收一个整形数为变量并返回一个迭代器及该迭代器的第一个元素。

>>> it = iter(range(5))
>>> x, it1 = peep(it)
>>> print x, list(it1)
0 [0, 1, 2, 3, 4]

问题 9: Python内置函数enumerate接收一个iteratable为参数,并返回一个迭代器,迭代器的每一个元素是由该iteratable的每一个值生成的键-值对 (index,value)。

>>> list(enumerate(["a", "b", "c"])
[(0, "a"), (1, "b"), (2, "c")]
>>> for i, c in enumerate(["a", "b", "c"]):
...     print i, c
...
0 a
1 b
2 c

写一个函数my_enumerate让它能像enumerate一样工作。

问题 10:实现一个函数izip让它能像itertools.izip一样工作。

延伸阅读

David Beazly 写的 Generator Tricks For System Programers 是进一步详尽介绍生成器和生成器表达式的极好材料。

译者注

  • 更新时间:2014-11-19
  • 本人翻译的初衷是为了自身学习和记录,翻译不好的地方,还望读者见谅.
  • 更多最新原创的译文,请关注本人的Github https://github.com/mainframer/Python-Doc-zh-CN,只翻译经典,只翻译精华。

Comments !